/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package clasificador.regresion;

import clasificador.ClasificadorEntrenado;
import datos.Atributo;
import datos.AtributoFinito;
import datos.Dato;
import datos.IBuilder;
import datos.MuestraClasificada;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 *
 * @author Simply
 */
public class ClasificadorRegresionLogistica<Clase> extends ClasificadorEntrenado<Clase> {

    private int dimension;
    private List<Atributo<Object>> linearization;
    
    private double[] regressionCoefficients;
    
    private Clase defaultChoice;
    private Clase alternateChoice;    
    
    private Integer trainingEpochs;
    private Boolean randomizeEntries;
    private Double learningCoefficient;
    private Double learningCoefficientDecay;

    private double scalarProduct(double[] coefficients, double[] datum) {
        double sum = coefficients[0];
        for (int i = 0; i < datum.length; i++) {
            sum += coefficients[i + 1] * datum[i];
        }
        return sum;
    }
    
    private double logistic(double[] coefficients, double[] datum) {
        return (1.0 / (1.0 + Math.exp(-scalarProduct(coefficients, datum))));
    }

    private double[] vectorize(Dato d) {
        double[] vector = new double[this.dimension];
        for (int i = 0; i < this.dimension; i++) {
            vector[i] = (double) d.getAtributo(linearization.get(i));
        }
        return vector;
    }

    public ClasificadorRegresionLogistica(MuestraClasificada<Clase> datosEntrenamiento, Map<String,Object> parametros) {
        super(datosEntrenamiento, parametros);        
    }

    private class CoefficientBuilder implements IBuilder<Map.Entry<double[], Clase>, double[]> {

        private Clase cls;
        private int dimension;
        private double[] coefficients;
        private double currentLearningCoefficient = learningCoefficient;
        
        int i = 0;

        public CoefficientBuilder(Clase c, int dimension) {
            super();
            this.cls = c;
            this.dimension = dimension;

            this.reset();
        }

        @Override
        public void add(Map.Entry<double[], Clase> e) {
            double[] dd = e.getKey();
            Clase c = e.getValue();            
            
            
            double estimatedValue = logistic(coefficients,dd);            
            double realValue = (this.cls.equals(c)?1.0:0.0);           
            
            
            /* No estoy seguro de si esto es en este orden o en el contrario */
            double error = +realValue-estimatedValue;            
            
            coefficients[0] += currentLearningCoefficient*error;
            for(int i = 0; i < dimension; i++)
            {
            	coefficients[i+1] += currentLearningCoefficient*error*dd[i];
            }            
            
        }

        @Override
        public double[] finish() {
            	return coefficients;        	
        }        
        
        @Override
        public void reset() {
            this.coefficients = new double[this.dimension + 1];
            
            for(int i = 0; i < this.dimension + 1; i++)
            {
            	/* En principio inicializar a cualquier cosa es igual de v�lido */
            	coefficients[i] = 1.0;
            }
        }
        
        public void nextEpoch() {
            this.currentLearningCoefficient *= learningCoefficientDecay;
        }
    }

    @Override
    protected void entrenar(MuestraClasificada<Clase> datosEntrenamiento) {
        AtributoFinito<Clase> cls;
        CoefficientBuilder builder;

        if (datosEntrenamiento.getClase() instanceof AtributoFinito<?>) {
            cls = (AtributoFinito<Clase>) datosEntrenamiento.getClase();
            
            if(cls.getRango().size() != 2) {
                throw new RuntimeException("Unable to use logistic regression for more than two classes.");
            }
            
            Object[] clases = cls.getRango().toArray();
            this.defaultChoice = (Clase)clases[0];
            this.alternateChoice = (Clase)clases[1];
            
            this.dimension = datosEntrenamiento.getAtributos().size()-1;

            this.linearization = new ArrayList<>();
            for (Atributo<?> attr : datosEntrenamiento.getAtributos()) {
            	if (!attr.equals(cls))
            	{
	                if (Double.class.isAssignableFrom(attr.getTipo())) {
	                    this.linearization.add((Atributo<Object>) attr);
	                } else {
	                    throw new RuntimeException("Attribute " + attr.getNombre() + " not continuous");
	                }
            	}
            }
            
            builder = new CoefficientBuilder(this.defaultChoice, this.dimension);
            List<Map.Entry<double[], Clase>> trainingData = new ArrayList<>();
            List<Integer> indices = new ArrayList<>();

            {
                int i = 0;
                for (Dato d : datosEntrenamiento.getDatos()) {
                    trainingData.add(
                            new AbstractMap.SimpleEntry(this.vectorize(d)
                            , d.getAtributo(cls)));
                    indices.add(i);
                    i ++;
                }
            }
            
            
            for (int epoch = 0; epoch < this.trainingEpochs; epoch++) {
                if (this.randomizeEntries) {
                    Collections.shuffle(indices);
                }
                for (int index : indices) {
                    builder.add(trainingData.get(index));
                }
                builder.nextEpoch();
            }
            
            this.regressionCoefficients = builder.finish();

            String eqRecta = new Double(regressionCoefficients[0]).toString();
            
            for(int i = 1; i < regressionCoefficients.length; i++)
            {
            	eqRecta += " + " + regressionCoefficients[i] + linearization.get(i-1).getNombre();
            }
            eqRecta += " = 0";
            System.out.println(eqRecta);
        } else {
            throw new RuntimeException("Unable to use logistic regression on non-finite attribute");
        }
    }

    @Override
    protected void inicializarParametros(Map<String,Object> parametros) {
    	Integer trainingEpochs = (Integer) parametros.get("trainingEpochs");
        Boolean randomizeEntries = (Boolean) parametros.get("randomizeEntries");
        Double learningCoefficient = (Double) parametros.get("learningCoefficient");
        Double learningCoefficientDecay = (Double) parametros.get("learningCoefficientDecay");
        Double smoothing = (Double) parametros.get("smoothing");
        
        if (trainingEpochs != null) this.trainingEpochs = trainingEpochs; else this.trainingEpochs = 100;
        if (randomizeEntries != null) this.randomizeEntries = randomizeEntries; else this.randomizeEntries = false;
        if (learningCoefficient != null) this.learningCoefficient = learningCoefficient; else this.learningCoefficient = 0.1;
        if (learningCoefficientDecay != null) this.learningCoefficientDecay = learningCoefficientDecay; else this.learningCoefficientDecay = 1.0;        
    }
    
    @Override
    public Clase clasificar(Dato d) {
    	
    	double[] dd = this.vectorize(d);
        
        if (scalarProduct(this.regressionCoefficients, dd) > 0.0) {
        	return this.defaultChoice;
        } else {
        	return this.alternateChoice;
        }
    }
}
